Overview
Maintaining consistent code style improves readability, reduces bugs, and makes collaboration easier. This guide covers style conventions for both Java (Spring Boot) and TypeScript (React) codebases.
Java / Spring Boot Style
General Conventions
Follow standard Spring Boot and Java conventions:
- Indentation: 4 spaces (no tabs)
- Line Length: Maximum 120 characters
- Encoding: UTF-8
- File Organization: One public class per file
Naming Conventions
Classes and Interfaces
Use PascalCase for class and interface names:
// Controllers
public class ProductoController { }
public class AuthController { }
// Services
public class ProductoServiceImpl implements IProductoService { }
// Entities
public class Producto { }
public class Categorias { }
// DTOs
public class ProductoDetalleDTO { }
public class LoginDTO { }
Methods and Variables
Use camelCase for methods and variables:
public class ProductoController {
private final ProductoServiceImpl productoService;
private final ProductoMapper productoMapper;
@GetMapping
public ResponseEntity<List<ProductoDetalleDTO>> listarTodos() {
List<Producto> productos = productoService.obtenertodoslosproductos();
return ResponseEntity.ok(productoMapper.toDTOlist(productos));
}
@PostMapping
public ResponseEntity<ProductoDetalleDTO> crear(@RequestBody ProductoDetalleDTO dto) {
Producto creado = productoService.crearProducto(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(productoMapper.toDTO(creado));
}
}
Constants
Use UPPER_SNAKE_CASE for constants:
public class AppConstants {
public static final String BASE_URL = "http://localhost:8080/api";
public static final int MAX_PRODUCTS_PER_PAGE = 50;
public static final String DEFAULT_CURRENCY = "EUR";
}
Controller Pattern
Follow REST conventions with proper HTTP methods and status codes:
@RestController
@RequestMapping("/api/productos")
public class ProductoController {
private final ProductoServiceImpl productoService;
private final ProductoMapper productoMapper;
public ProductoController(ProductoMapper productoMapper, ProductoServiceImpl productoService) {
this.productoMapper = productoMapper;
this.productoService = productoService;
}
// GET /api/productos - List all
@GetMapping
public ResponseEntity<List<ProductoDetalleDTO>> listarTodos() {
List<Producto> productos = productoService.obtenertodoslosproductos();
return ResponseEntity.ok(productoMapper.toDTOlist(productos));
}
// GET /api/productos/{id} - Get by ID
@GetMapping("/{id}")
public ResponseEntity<ProductoDetalleDTO> obtenerPorId(@PathVariable Long id) {
return productoService.obtenerProductoPorId(id)
.map(p -> ResponseEntity.ok(productoMapper.toDTO(p)))
.orElse(ResponseEntity.notFound().build());
}
// POST /api/productos - Create
@PostMapping
public ResponseEntity<ProductoDetalleDTO> crear(@RequestBody ProductoDetalleDTO dto) {
Producto creado = productoService.crearProducto(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(productoMapper.toDTO(creado));
}
// PUT /api/productos/sku/{sku} - Update
@PutMapping("/sku/{sku}")
public ResponseEntity<ProductoDetalleDTO> actualizar(
@PathVariable String sku,
@RequestBody ProductoDetalleDTO dto) {
Producto actualizado = productoService.actualizarProducto(sku, dto);
return ResponseEntity.ok(productoMapper.toDTO(actualizado));
}
// DELETE /api/productos/{id} - Delete
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminar(@PathVariable Long id) {
productoService.borrarProducto(id);
return ResponseEntity.noContent().build();
}
}
Use constructor injection instead of field injection for better testability and immutability.
Service Layer Pattern
Use @Service annotation and implement interfaces:
@Service
public class ProductoServiceImpl implements IProductoService {
private final ProductoRepository productoRepository;
private final ProductoMapper productoMapper;
public ProductoServiceImpl(ProductoMapper productoMapper, ProductoRepository productoRepository) {
this.productoMapper = productoMapper;
this.productoRepository = productoRepository;
}
@Override
@Transactional
public Producto crearProducto(ProductoDetalleDTO dto) {
if (productoRepository.existsBySku(dto.getSku())) {
throw new IllegalArgumentException("Ya existe un producto con el SKU: " + dto.getSku());
}
Producto producto = productoMapper.toEntity(dto);
return productoRepository.save(producto);
}
@Override
@Transactional(readOnly = true)
public Optional<Producto> obtenerProductoPorId(Long id) {
return productoRepository.findById(id);
}
@Override
@Transactional
public Producto actualizarProducto(String sku, ProductoDetalleDTO dto) {
Producto productoActualizado = productoRepository.findBySku(sku)
.orElseThrow(() -> new RuntimeException("Producto no encontrado con Sku " + sku));
productoMapper.updatefromEntity(dto, productoActualizado);
return productoRepository.save(productoActualizado);
}
}
Always use @Transactional for service methods that modify data. Use @Transactional(readOnly = true) for read-only operations to optimize performance.
Entity Pattern
Use JPA annotations without Lombok for entities (current style):
@Entity
@Table(name = "Producto")
public class Producto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long producto_id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "categoria_id")
private Categorias categoria;
@Column(name = "nombre", length = 155, nullable = false)
private String nombre;
@Column(name = "sku", unique = true, nullable = false, length = 50)
private String sku;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "cantidad", column = @Column(name = "Cantidad", nullable = false, scale = 2)),
@AttributeOverride(name = "moneda", column = @Column(name = "Moneda", nullable = false, length = 3))
})
private Precio precio;
// Default constructor
public Producto() {
}
// All-args constructor
public Producto(Categorias categoria, String nombre, String sku, Precio precio) {
this.categoria = categoria;
this.nombre = nombre;
this.sku = sku;
this.precio = precio;
}
// Getters
public Long getProducto_id() {
return producto_id;
}
public String getNombre() {
return nombre;
}
// Setters
public void setNombre(String nombre) {
this.nombre = nombre;
}
// Business methods
public void vincularCategoria(Categorias categoria) {
if (this.categoria == categoria) {
return;
}
if (this.categoria != null) {
this.categoria.getProductos().remove(this);
}
this.categoria = categoria;
if (categoria != null && !categoria.getProductos().contains(this)) {
categoria.getProductos().add(this);
}
}
}
Lombok Usage
While the project includes Lombok, it’s primarily used for DTOs and utility classes:
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TokenDTO {
private String token;
}
@Data
public class ApiError {
private int status;
private String message;
private LocalDateTime timestamp;
}
Lombok annotations to use:
@Data - Generates getters, setters, toString, equals, and hashCode
@NoArgsConstructor - Generates no-argument constructor
@AllArgsConstructor - Generates constructor with all fields
@Builder - Implements builder pattern
MapStruct Usage
Use MapStruct for entity-DTO mapping:
@Mapper(componentModel = "spring", uses = { CategoriaMapperResumen.class })
public interface ProductoMapper {
@Mapping(source = "categoria", target = "categoria")
@Mapping(source = "precio.cantidad", target = "precioCantidad")
@Mapping(source = "precio.moneda", target = "precioMoneda")
@Mapping(source = "dimensiones.alto", target = "dimensionesAlto")
@Mapping(source = "dimensiones.ancho", target = "dimensionesAncho")
@Mapping(source = "dimensiones.profundidad", target = "dimensionesProfundo")
@Mapping(source = "imagenUrl", target = "imagen_url")
ProductoDetalleDTO toDTO(Producto producto);
@Mapping(target = "precio", expression = "java(new Precio(dto.getPrecioCantidad(), dto.getPrecioMoneda()))")
@Mapping(target = "dimensiones", expression = "java(new Dimensiones(dto.getDimensionesAlto(), dto.getDimensionesAncho(), dto.getDimensionesProfundo()))")
Producto toEntity(ProductoDetalleDTO dto);
List<ProductoDetalleDTO> toDTOlist(List<Producto> productos);
void updatefromEntity(ProductoDetalleDTO dto, @MappingTarget Producto producto);
}
After modifying MapStruct interfaces, always recompile the project to regenerate implementation classes:
Use JavaDoc for public APIs:
/**
* Service for managing product operations.
* Handles CRUD operations and business logic for products.
*/
@Service
public class ProductoServiceImpl implements IProductoService {
/**
* Creates a new product in the system.
*
* @param dto the product details
* @return the created product entity
* @throws IllegalArgumentException if SKU already exists
*/
@Override
@Transactional
public Producto crearProducto(ProductoDetalleDTO dto) {
if (productoRepository.existsBySku(dto.getSku())) {
throw new IllegalArgumentException("Ya existe un producto con el SKU: " + dto.getSku());
}
Producto producto = productoMapper.toEntity(dto);
return productoRepository.save(producto);
}
}
TypeScript / React Style
General Conventions
- Indentation: 4 spaces (as configured in project)
- Line Length: Maximum 100 characters
- Quotes: Single quotes for strings
- Semicolons: Required
Naming Conventions
Components
Use PascalCase for React components:
// Components
export default function ProductoCard({ producto }: Props) { }
export default function Navbar() { }
// Pages
export default function Home() { }
export default function ProductDetail() { }
Variables and Functions
Use camelCase for variables, functions, and hooks:
const [productos, setProductos] = useState<Producto[]>([]);
const [isLoading, setIsLoading] = useState(false);
function fetchProductos() {
// ...
}
const handleSubmit = (event: FormEvent) => {
// ...
};
Types and Interfaces
Use PascalCase for TypeScript types and interfaces:
export interface Producto {
producto_id: number;
sku: string;
nombre: string;
descripcion: string;
precioCantidad: number;
precioMoneda: string;
stock: number;
imagen_url: string;
categoria: CategoriaResumen;
}
export interface LoginDTO {
email: string;
password: string;
}
export type EstadoPedido = 'PENDIENTE' | 'CONFIRMADO' | 'ENVIADO' | 'ENTREGADO' | 'CANCELADO';
Component Pattern
Use functional components with TypeScript:
import { Link } from 'react-router-dom';
import type { Producto } from '../types';
import './ProductoCard.css';
interface Props {
producto: Producto;
}
export default function ProductoCard({ producto }: Props) {
const precio = producto.precioCantidad?.toFixed(2) ?? '—';
return (
<Link to={`/productos/${producto.producto_id}`} className="producto-card">
<div className="producto-card__img-wrap">
<img
src={producto.imagen_url || 'https://placehold.co/400x300?text=Sin+imagen'}
alt={producto.nombre}
className="producto-card__img"
loading="lazy"
/>
{producto.es_destacado && (
<span className="producto-card__badge">Destacado</span>
)}
</div>
<div className="producto-card__body">
{producto.categoria && (
<span className="producto-card__categoria">{producto.categoria.nombre}</span>
)}
<h3 className="producto-card__nombre">{producto.nombre}</h3>
<p className="producto-card__precio">
{precio} <span className="producto-card__moneda">{producto.precioMoneda}</span>
</p>
</div>
</Link>
);
}
API Client Pattern
Create typed API functions:
const BASE_URL = 'http://localhost:8080/api';
function getToken(): string | null {
return localStorage.getItem('token');
}
function authHeaders(): HeadersInit {
const token = getToken();
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
export async function apiFetch<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
...authHeaders(),
...(options.headers ?? {}),
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: 'Error desconocido' }));
throw new Error(error.message ?? `Error ${res.status}`);
}
return res.json() as Promise<T>;
}
CSS Class Naming
Use BEM (Block Element Modifier) convention:
/* Block */
.producto-card { }
/* Elements */
.producto-card__img-wrap { }
.producto-card__img { }
.producto-card__body { }
.producto-card__nombre { }
.producto-card__precio { }
/* Modifiers */
.producto-card__badge--agotado { }
.producto-card--featured { }
Use JSDoc for exported functions and complex logic:
/**
* Fetches all products from the API.
* @returns Promise resolving to array of products
* @throws Error if request fails
*/
export async function fetchProductos(): Promise<Producto[]> {
return apiFetch<Producto[]>('/productos');
}
/**
* Calculates the total price of cart items.
* @param items - Array of cart items
* @returns Total price including all items
*/
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + (item.producto.precioCantidad * item.cantidad), 0);
}
ESLint Configuration
The project uses ESLint for code quality. Run linting:
Key rules:
- No unused variables
- Consistent spacing and indentation
- Proper React hooks usage
- TypeScript type safety
Use your IDE’s auto-formatting (Ctrl+Alt+L in IntelliJ):
- 4 spaces indentation
- Opening braces on same line
- One statement per line
Ensure consistent formatting:
- 4 spaces indentation
- Single quotes
- Trailing commas in objects/arrays
- Semicolons required
Best Practices
Java Best Practices:
- Use constructor injection over field injection
- Mark service methods with
@Transactional
- Use
Optional for methods that may return null
- Validate input in service layer
- Use specific exception types
TypeScript Best Practices:
- Always define TypeScript interfaces for data structures
- Use optional chaining (
?.) and nullish coalescing (??)
- Prefer
const over let
- Extract complex logic into custom hooks
- Use semantic HTML elements
Additional Resources